home *** CD-ROM | disk | FTP | other *** search
- _Creating User-Installable Device Drivers in MS-DOS 2.0+_
-
- Bruce Bordner, 1985
- 1713 4th Avenue
- Asbury Park, NJ 07712
-
-
-
- Prior to version 2.0, driver programs to support new (or non-IBM)
- peripherals required some complex and ugly programming to interface with
- DOS. This became so much of a problem that it was fixed in the first major
- revision (2.0) by providing a "legal" way to install device drivers and
- interface with the DOS I/O functions. This was primarily intended as a
- convenience for OEMs, but Microsoft did include a chapter (14) about the
- subject in the DOS manual. I haven't found any better source on the
- subject; which means that I found practically no other information. [Only
- other source: "Modifying MS-DOS Device Drivers" by Mike Higgins, Computer
- Language 3/85 - a good article which can clarify the DOS documentation, but
- does not treat several points explained here.] Assuming for the moment that
- driver software can be home-brewed, it is not an obviously useful technique.
- You could make your own driver for a 500 megabyte drive rather than waiting
- for the manufacturer to do it, but this is a last-ditch move. However, a
- driver does not necessarily have to be controlling a physical device. A DOS
- device driver must be a COM file, which limits the code space to 64K total.
- It has a specified header and function call structure. DOS will only make
- and accept I/O operations which are given in the manual. You cannot return
- an error code other than those used by DOS, and most of those are not
- returned to your calling program. Other than that, you can do as you like.
- This opens up many possibilities. Driver software becomes a part of the
- memory-resident sections of DOS during the boot-up operation. It becomes
- another resource available to any of your programs. The classic example, as
- given in the DOS manual, is a ram-based disk simulator. This is a "virtual"
- device, where the code and data is seen as a package by the operating system
- and user programs. This module can perform any function which you can fit
- into a COM program, with predefined interfaces to DOS, other programs, and
- other device drivers. Although Microsoft built some limiting assumptions
- into DOS which make it difficult to implement certain functions, many
- possibilities remain. One option which I would like to explore is to
- offload a driver to an outboard processor for concurrent background
- operation.
-
- _DOS Function Calls for Device Drivers_
-
- In order to keep things simple, I only used the "extended file management"
- (version 2.0) functions under system interrupt 21H. These are used by the
- "fread()" and "fwrite()" functions of the C86 C compiler. The primary
- functions affecting drivers are:
-
- 3D Open a file or device, return a 16-bit file handle in register
- AX.
-
- 3E Close the file or device associated with the handle in BX.
-
- 3F Read from a file or device. The following registers must be
- loaded as indicated:
- BX => file handle of device
- CX => number of bytes to read ( 64K max)
- DX => segment offset of your data buffer storage
- DS => segment of your data buffer
- Reads CX bytes from a device into the buffer address given.
- *** What is not explained by Microsoft is that DOS actually
- requests only one byte per call to the device driver, making
- CX number of calls. This has all sorts of ugly side effects,
- which will become evident in the description of my sample
- driver. Microsoft has apparently built in the assumption that
- all character-oriented devices talk to fairly slow serial
- hardware, like printers. If I read this right, DOS has the
- ability to respond to interrupts between each byte transferred,
- but it does make things clumsy. DOS apparently increments the
- DS:DX buffer location after each call. The driver will be
- re-run from START for each byte, and must maintain its own
- data pointers to maintain synchronism with the DOS transfer.
-
- 40 Write to a file or device.
- Same as above, but data is transferred to your buffer area.
- *** Same warning.
-
- 44 I/O Control for Devices. This function has 8 subfunction codes
- which are used for some rudimentary device controls.
- Subfunction 2 is to read CX number of bytes from the "control
- channel" of the device, with the same register settings as for
- the normal read. The stated purpose is to provide a way of
- reading device driver status rather than data from the device
- itself. However, up to 64K bytes made be transferred per call.
- What your device does with it is up to you.
- Subfunction 3 is the corresponding write call.
- *** For these calls, DOS actually requests CX bytes from the
- device on one call. This is used in the second version of my
- sample driver, which is much simpler than the standard
- read-write calls used in the first version.
-
- _How DOS Translates Your Read/Write Function Calls into Device Driver Requests_
-
- When any of the above functions is called by your application program, DOS
- develops a data structure called the "Request Header" by the manual. This
- structure consists of a 13-byte defined header which may be followed by
- other data bytes depending on the function requested. The fixed part of the
- request header is as follows:
-
- _BYTE_ _PURPOSE_
- 0 Length in bytes of the total request header (0-255)
- 1 Unit code, used to determine subunit to use in block devices
- (not used for character devices)
- 2 Command code (0-12) to activate specific device function
- 3-4 Status word, returned by the driver
- 5-12 The manual states that this area is "reserved for DOS".
- Another source indicates that this consists of two double-word
- (4-byte) pointers to be used to maintain a linked list of
- request headers for this device and a list of all current
- device requests being processed by DOS. This is apparently
- in the works for a future concurrent-DOS.
-
- The 13 command codes are detailed on pages 14-12 of the manual; only the
- following are used by the character devices explained in this paper:
-
- _CODE_ _FUNCTION_
- 0 INIT - perform all initialization required at DOS boot time
- to install the driver and set local driver variables.
- 3 IOCTL INPUT - read a specified number of bytes from the
- device driver's IO control channel.
- 4 INPUT - normal device "read". Reads a number of bytes from
- the device your driver is controlling.
- 8 OUTPUT - normal device "write" call from user program.
- 12 IOCTL OUTPUT - write bytes to driver control channel.
-
- For each of these function calls, the driver receives the following:
-
- INIT: This function must be built into any driver program. It is called only
- by DOS during boot time, to reserve the system memory needed to hold the
- driver and to link the driver into the set of active devices managed by DOS.
- DOS sends: 13-byte request header
- BYTE number of units (not used by char devices)
- DWORD ending address of driver
- DWORD pointer to BPB array (not used by char devices)
- The driver program must load the ending address at a minimum; any local
- initialization may also be performed at this time.
-
- INPUT, OUTPUT, IOCTL INPUT, or IOCTL OUTPUT:
- For all of these, DOS sends:
- 13-byte request header
- BYTE media descriptor (not used for char devices)
- DWORD offset and segment of the data buffer in calling program
- WORD number of bytes to transfer in this call
- WORD starting sector (not used for char devices)
- The driver must perform the requested read or write function, set the "number
- of bytes to transfer" location to the number actually done, and set the status
- word in the request header to indicate any errors.
-
- The actual use of these structures will be detailed in the driver function
- description.
-
- _Required Structure for a Device Driver_
-
- Listing 1 (DOSDEV.ASM) is a template containing the minimum requirements for
- a character-oriented device driver. This is detailed in pages 14-3 to 14-8
- of the DOS manual. The driver program must meet the requirements of a
- normal COM file. However, COM files usually start with an ORG 100H to allow
- room for the DOS Program Segment Prefix structure. For a driver, you must
- use ORG 0, as the PSP is not used. The Device Header data structure must be
- the first object defined in your file. It consists of:
- DWORD Pointer
- to the next device driver currently installed. This
- should be initialized to -1, DOS will fill this field as
- necessary during system initialization (boot).
- WORD Device attribute. I used C000H to indicate that this is a
- character device with IOCTL capability. This field is also
- used to indicate if this device is to be the standard output
- or input device.
- WORD Pointer to "device strategy" function in the driver. This
- function is called whenever a request is made to the driver,
- and must store the location of the request header from DOS.
- WORD Pointer to function which activates driver routines to perform
- the command in the current request header. This is called by
- DOS after the call to the strategy function, and should reset
- to the request header address stored by "strategy", to allow
- for the possibility of interrupts between the two calls.
- 8-BYTES Name field. For character devices, fill this with the name
- which you must use when opening the device.
-
- After this structure, you may include any local data definitions needed for
- the internal operation of your driver. The DOSDEV example includes only the
- minimum; a pointer to the request header and a table of addresses of the
- functions which will be called by the command code from DOS. The function
- addresses are arranged according to their calling function code (0 to 12) so
- that the function router can use the DOS command code as an offset into this
- table.
-
- _Required Device Driver Functions_
-
- For simplicity, I will discuss these functions as given in the DOSDEV.ASM
- listing.
-
- XDV STRAT: This function is called directly by DOS when a request has been
- made to use this device. Its only purpose is to save a segment and offset
- pointer to the request header. At the time DOS calls the device, the segment
- of the request header is in register ES and the offset is in register BX.
- These values are copied into the variables RH SEG and RH OFF. The fact that
- Microsoft calls this a "device strategy" function leads me to believe that
- more complex processing will be required in this function when DOS becomes
- multi-user or multi-processing oriented.
-
- XDV FUNC: This is called by DOS immediately after XDV STRAT. The function
- pushes all machine registers to save the current data until the device has
- finished the requested operation. Data segment register DS is set to the
- Code segment value, as all local variables exist in the code segment.
- Registers ES and BX are loaded from RH SEG and RH OFF to reset them to the
- start of the DOS request header. The command code from the request header
- (at ES:[BX+2] ) is then used as an offset into the function address table
- FUNTAB to initiate the driver function requested. In DOSDEV, only the INIT
- function has been coded, all others drop out to EXIT after setting the
- status word of the request header to "done; no error". All you need to do
- is fill in the function code for any driver function you intend to use.
-
- INIT: When DOS is booted, it reads your CONFIG.SYS file to determine which
- programs to install as device drivers (DEVICE=filename.ext). After loading
- the file image into memory, DOS sends a request header with the command code
- "0" to the device. The INIT function must load an offset (at ES:[BX+14])
- and segment value (at ES:[BX+16]) into the request header to indicate the
- ending address for the driver program, including space for any memory used
- as a virtual device. The function may also do any initial variable setting
- within the driver. INIT then exits back to DOS, which uses the address
- given to set the boundary of DOS including the new driver storage.
-
- EXIT: This function restores all machine registers and returns to DOS.
-
- _Examples of Character-Oriented Device Drivers_
-
- Listing 1 (STKDEV.ASM) and 2 (STKDEV2.ASM) show the use of a virtual device
- driver to implement a "stack". User programs may "push" bytes or entire
- records by writing them to the device, and "pop" them with a read request.
- I/O control calls are used to set the record size to be used by the driver.
- This may not be very useful in itself, but this example shows solutions to
- most of the problems without being difficult to read.
- STKDEV is constructed in the recommended fashion; I got much of the code
- from the example device driver in the DOS manual. Read and write calls from
- the user program activate the functions INPUT and OUTPUT, while IOCTL IN and
- IOCTL OUT are used to read and write the record size setting. I developed
- the first version in a few days, but then spent two months of spare time
- trying to find out why it wouldn't work. It's an undocumented feature of
- MS-DOS, although I can see some hints of it in the manual - now that I know
- what to look for. When your user program makes a read or write call to a
- device, you send DOS the number of bytes to transfer, which may be 1 to 64K.
- You make one call to DOS (interrupt 21H). The request header for I/O
- contains a full word to contain the byte count sent from DOS. I made the
- mistake of assuming that when I make a 10 byte I/O request to my driver, the
- driver would see a 10 byte count. Actually, it sees 10 unrelated 1-byte
- requests from DOS. STKDEV's INPUT and OUTPUT functions show the effects. I
- had to establish two new variables (NUM2READ and NUM2WRITE) to keep track of
- how many bytes had been transferred, so that the driver would know if it was
- done with a "record". This is required because the "top of stack" pointer
- (CURRENT) is set to the next free address following the last byte written.
- A "pop" operation (INPUT) requires decrementing the pointer by the record
- size, transferring a full record in the byte order written, then resetting
- the pointer back to the used record's start to allow overwriting and
- repeated "pops". There must be an easier way to do this, but I think this
- mess shows the problems more clearly.
- STKDEV2 uses IOCTL functions rather than the standard I/O. On IOCTL
- calls, DOS sends the full byte count in the request header. This made
- things simpler in my driver code, but complicated my user programs by
- requiring custom read/write functions. Take your choice. DOS apparently
- starts at the buffer address which your program supplies in the I/O call,
- transferring one byte with a request to the specified driver, then
- incrementing the buffer pointer by 1, and repeating until the specified
- number of bytes is copied. The device driver must track this indexing
- carefully in some applications, for others it may not matter.
-
- Mike Higgins' article included many debugging tips. One of them is the
- "yell" macro at the beginning of STKDEV. This displays one character on the
- screen by writing directly to the video memory. If you use DOS function
- calls to display the status of your driver, DOS will overwrite the request
- header which your driver has started processing. I have left "yell"
- invocations throughout the function code; it was this macro that finally
- showed me what my device was receiving from DOS.
-
- Functional Description of STKDEV.ASM:
-
- The procedure and device name is XSTK, which must be used when opening the
- device for I/O. It is assembled and linked normally, then use EXE2BIN to
- convert the EXE file to COM form. I used EXE2BIN STKDEV.EXE XSTK.SYS,
- changing the file name because any references to XSTK once it is installed
- cause weirdness. The CONFIG.SYS file must contain DEVICE=XSTK.SYS. Reboot
- and XSTK has added 32K+ to memory-resident DOS.
-
- XSTK STRAT:
- This is the "device strategy" function, which is called first by DOS for
- every request header. DOS has set ES and BX to the address of the request
- header; these are stored RH SEG and RH OFF to ensure that the driver will be
- able to find the request header. It may be omitted for DOS 2.0.
-
- XSTK FUNC:
- DOS calls this entry point second on all device requests. The call is a
- signal to begin processing the data in the request header. DOS is now
- suspended (in this version) until your device returns to it. All machine
- registers are saved on the stack, and ES and BX are reloaded to the address
- stored by XSTK STRAT. The command code at ES:[BX+2] is used as an index to
- jump to the requested function.
-
- INIT:
- This function is called only by DOS, only during installation (boot) time.
- As it will not be needed while the device is operating, INIT could be
- located after the end address returned to DOS, saving some memory. STORAGE
- is the variable marking the end of the XSTK code. However, I add 32K for
- stack storage. This value is then copied to the request header and returned
- to DOS for memory allocation. Local variables are set to default "stack
- empty" values.
-
- IOCTL IN:
- Used to read the current record size from the driver into the calling
- program's data buffer. In order to use the REP MOVSB instruction, CX is set
- to the requested byte count, DS and SI point to the internal variable
- RECSIZE, ES and DI point to the buffer address contained in the request
- header. SI and DI are incremented by the REPeat prefix until CX bytes have
- been transferred. ES and BX are then reset to the request header address.
-
- IOCTL OUT:
- Write a new record size to the device. Same deal as above, backward.
-
- INPUT:
- Processes read requests. The double word at ES:[BX+14] contains the address
- of the data buffer in the calling program, and the word at ES:[BX+18] is the
- byte count for the request. This value will always be 1 for DOS 2.0, but
- this is subject to change. The first process required is to check whether
- the previous write (OUTPUT) completed storing RECSIZE bytes. This is done
- by checking the NUM2WRITE variable. If NUM2WRITE is not 0, the CURRENT
- pointer is set to a record boundary before reading. Next, INPUT checks to
- see if it is in the process of reading a record or if it is starting a new
- record. If NUM2READ is 0, INPUT must reset CURRENT to the start of the last
- record written. At this time, INPUT checks CURRENT against BOTMEM to ensure
- that reads will not go past the bottom of the stack space. I tried to
- return an error code of 30H, to give my calling program a different error
- than those used by DOS. However, DOS apparently checks this value against
- the approved list, and I get a "Disk drive error" on the display. So, it
- appears that only the given error codes will be sent to calling programs.
- The actual read transfer is set up at PULLIT. Again, I used the REP MOVSB
- instruction, even though DOS will only call for one byte per request header.
- CX is loaded with the count from the request header, DS and SI have been set
- to the proper address in the stack storage, ES and DI are set to the data
- buffer address of the calling program. NUM2READ is decremented on each
- request. While NUM2READ is not 0, the value of SI is stored in CURRENT; SI
- has been incremented by the REP MOVSB to point at the next byte of the
- record. If NUM2READ is 0, a full record has been read and CURRENT must be
- reset to the starting address of the record. Finally, ES and BX are reset
- to point to the DOS request header. The status word of the request header
- is filled with the code for "done; no error" and the process completes
- through EXIT.
-
- OUTPUT:
- Similar to INPUT, except that CURRENT always increments.
-
-
- Description of STKDEV2:
-
- This version reverses the use of IOCTL and INPUT/OUTPUT. Most of the code
- is the same as STKDEV, but the variables NUM2READ and NUM2WRITE are no
- longer needed, as the DOS request header will request the actual number of
- bytes given by the calling program. THerefore, the driver implicitly knows
- that each request will consist of a complete record. If you compare IOCTL
- IN with INPUT, and IOCTL OUT with OUTPUT of STKDEV, it is obvious that this
- approach was easier to code for this application.
-
-
- _Testing the Sample Device Drivers_
-
- Listing 4 (TXSTK.C) is a C program to perform simple test calls to STKDEV.
- Listing 5 (TXSTK2.C) tests STKDEV2 by using IOCTL calls in place of the
- read/write system calls used in TXSTK.
-
- TXSTK uses segread() to determine the DS segment value of itself. This is
- used to pass DOS the segment value of the data buffer "instr". Then XSTK is
- opened. I used sysint21() calls instead of fopen, fwrite() and fread() just
- to simplify matters. "Outstr" is then written to XSTK. Although I fill
- callregs.cx with the count of 5, I know that DOS will make 5 one-byte calls.
- XSTK is currently set to the default RECSIZE of one, so a write and
- corresponding read produces "olleH" from my string "Hello" written. I then
- use IOCTL calls to reset XSTK's RECSIZE to 5 bytes. Although the following
- write and read are in the same form as before, XSTK now knows to treat input
- as 5-byte records. So, "Hello" returns "Hello".
-
- TXSTK2 is the same through opening XSTK. However, the first byte-at-a-time
- write/read must use a loop to cycle through the 5 characters of the output
- and input strings. After resetting XSTK's RECSIZE to 5 using I/O calls, the
- write/read calls request 5 bytes, which is now processed in one call to
- XSTK. The strings are returned as with TXSTK; "olleH" and "Hello".
-
-